/*
 * Copyright (c) 2003-2023 The Apereo Foundation
 *
 * Licensed under the Educational Community License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *             http://opensource.org/licenses/ecl2
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.sakaiproject.unboundid;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;

import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.user.api.UserEdit;
import org.sakaiproject.user.detail.ValueEncryptionUtilities;

/**
 * Extension for {@link SimpleLdapAttributeMapper}.
 * Adds logic to include encrypted CandidateId and additional info into the Sakai user object.
 */
@Slf4j
public class SimpleLdapCandidateAttributeMapper extends SimpleLdapAttributeMapper
{
    private static final String USER_PROP_CANDIDATE_ID = "candidateID";
    private static final String USER_PROP_ADDITIONAL_INFO = "additionalInfo";
    private static final String USER_PROP_STUDENT_NUMBER = "studentNumber";
    private static final String EMPTY = "";

    @Setter private ServerConfigurationService scs;
    @Setter private ValueEncryptionUtilities encryption;

    @Setter private int candidateIdLength;
    @Setter private int additionalInfoLength;
    @Setter private int studentNumberLength;

    /**
     * Completes configuration of this instance.
     *
     * <p>Initializes internal mappings to a copy of {@link AttributeMappingConstants#DEFAULT_ATTR_MAPPINGS} if the current map is empty. Initializes user
     * type mapping strategy to a {@link EmptyStringUserTypeMapper} if no strategy has been specified.</p>
     * <p>This defaulting enables UDP config forward-compatibility.</p>
     *
     */
    public void init()
    {
        log.debug("initializing");

        if (MapUtils.isEmpty(getAttributeMappings()))
        {
            setAttributeMappings(AttributeMappingConstants.CANDIDATE_ATTR_MAPPINGS);
            log.debug("creating default attribute mappings");
        }

        if (getUserTypeMapper() == null)
        {
            setUserTypeMapper(new EmptyStringUserTypeMapper());
            log.debug("created default user type mapper [mapper = {}]", getUserTypeMapper());
        }
        if (getValueMappings() == null)
        {
            setValueMappings(Collections.EMPTY_MAP);
            log.debug("created default value mapper [mapper = {}]", getValueMappings());
        }
        else
        {
            // Check we have good value mappings and throw any out that aren't (warning user).
            Iterator<Entry<String, MessageFormat>> iterator = getValueMappings().entrySet().iterator();
            while (iterator.hasNext())
            {
                Entry<String, MessageFormat> entry = iterator.next();
                if (entry.getValue().getFormats().length != 1)
                {
                    iterator.remove();
                    log.warn("Removed value mapping as it didn't have one format: {} -> {}", entry.getKey(), entry.getValue().toPattern());
                }
            }
        }
    }

    /**
     * Caches the given Map reference and takes a snapshot of the values therein for future use by {@link #getSearchResultAttributes()}. (using super method)
     * @param attributeMappings
     * @see #getAttributeMappings()
     */
    public void setAttributeMappings(Map<String,String> attributeMappings)
    {
        if (MapUtils.isEmpty(attributeMappings))
        {
            super.setAttributeMappings(AttributeMappingConstants.CANDIDATE_ATTR_MAPPINGS);
        }
        else
        {
            super.setAttributeMappings(attributeMappings);
        }
    }

    /**
     * Straightforward {@link LdapUserData} to {@link org.sakaiproject.user.api.UserEdit} field-to-field mapping, including properties.
     * @param userData
     * @param userEdit
     */
    public void mapUserDataOntoUserEdit(LdapUserData userData, UserEdit userEdit)
    {
        Properties userDataProperties = userData.getProperties();
        ResourceProperties userEditProperties = userEdit.getProperties();
        if (userDataProperties != null)
        {
            Object o_prop = userDataProperties.get(AttributeMappingConstants.CANDIDATE_ID_ATTR_MAPPING_KEY);
            if (o_prop != null)
            {
                if (o_prop instanceof String)
                {
                    userEditProperties.addProperty(USER_PROP_CANDIDATE_ID, encryption.encrypt((String) o_prop, candidateIdLength));
                }
                else if (o_prop instanceof List)
                {
                    Set<String> propertySet = new HashSet<>();

                    // Remove duplicate values
                    for (String value : (List<String>) o_prop)
                    {
                        propertySet.add(value);
                    }

                    // Add candidate ID, if there is only one value
                    if (propertySet.size() == 1)
                    {
                        for (String value : propertySet)
                        {
                            userEditProperties.addProperty(USER_PROP_CANDIDATE_ID, encryption.encrypt(value, candidateIdLength));
                        }
                    }
                }

                userDataProperties.remove(AttributeMappingConstants.CANDIDATE_ID_ATTR_MAPPING_KEY);
            }

            o_prop = userDataProperties.get(AttributeMappingConstants.ADDITIONAL_INFO_ATTR_MAPPING_KEY);
            if (o_prop != null)
            {
                if (o_prop instanceof String)
                {
                    userEditProperties.addPropertyToList(USER_PROP_ADDITIONAL_INFO, encryption.encrypt((String) o_prop, additionalInfoLength));
                }
                else if (o_prop instanceof List)
                {
                    List<String> propertyList = new ArrayList<>();

                    // Remove duplicate values (maintain order)
                    for (String value : (List<String>) o_prop)
                    {
                        if (!propertyList.contains(value))
                        {
                            propertyList.add(value);
                        }
                    }
                    for (String value : propertyList)
                    {
                        userEditProperties.addPropertyToList(USER_PROP_ADDITIONAL_INFO, encryption.encrypt(value, additionalInfoLength));
                    }
                }

                userDataProperties.remove(AttributeMappingConstants.ADDITIONAL_INFO_ATTR_MAPPING_KEY);
            }

            o_prop = userDataProperties.get(AttributeMappingConstants.STUDENT_NUMBER_ATTR_MAPPING_KEY);
            if (o_prop != null)
            {
                if (o_prop instanceof String)
                {
                    addStudentNumberProperty((String) o_prop, userEditProperties);
                }
                else if (o_prop instanceof List)
                {
                    Set<String> propertySet = new HashSet<>();

                    // Remove duplicate values
                    for (String value : (List<String>) o_prop)
                    {
                        propertySet.add(value);
                    }

                    // Add student number, if there is only one value
                    if (propertySet.size() == 1)
                    {
                        for (String value : propertySet)
                        {
                            addStudentNumberProperty(value, userEditProperties);
                        }
                    }
                }

                userDataProperties.remove(AttributeMappingConstants.STUDENT_NUMBER_ATTR_MAPPING_KEY);
            }

            // Make sure we have a value for the encrypted properties.
            if (StringUtils.isEmpty(userEditProperties.getProperty(USER_PROP_CANDIDATE_ID)))
            {
                userEditProperties.addProperty(USER_PROP_CANDIDATE_ID, encryption.encrypt(EMPTY, candidateIdLength));
            }
            if (userEditProperties.getPropertyList(USER_PROP_ADDITIONAL_INFO)!= null && userEditProperties.getPropertyList(USER_PROP_ADDITIONAL_INFO).isEmpty())
            {
                userEditProperties.addPropertyToList(USER_PROP_ADDITIONAL_INFO, encryption.encrypt(EMPTY, additionalInfoLength));
            }
            if (userEditProperties.getPropertyList(USER_PROP_STUDENT_NUMBER)!= null && userEditProperties.getPropertyList(USER_PROP_STUDENT_NUMBER).isEmpty())
            {
                addStudentNumberProperty(EMPTY, userEditProperties);
            }

        super.mapUserDataOntoUserEdit(userData, userEdit);
        }
    }

    private void addStudentNumberProperty(String number, ResourceProperties userEditProperties)
    {
        String studentNumber = number;
        if (scs.getBoolean(AttributeMappingConstants.SYSTEM_PROP_ENCRYPT_NUMERIC_ID, true))
        {
            studentNumber = encryption.encrypt(studentNumber, studentNumberLength);
        }

        userEditProperties.addProperty(USER_PROP_STUDENT_NUMBER, studentNumber);
    }
}
